package ferp.core.game;

import com.google.gson2.Gson;
import ferp.core.ai.strategy.Strategy;
import ferp.core.card.Card;
import ferp.core.card.Deck;
import ferp.core.log.Log;
import ferp.core.mode.Mode;
import ferp.core.player.*;
import ferp.core.state.State;
import ferp.core.tutorial.Scenario;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

/**
 * User: igorgok
 * Date: 5/19/11 2:34 PM
 */
public class Game
{
  public static void log(Game g, String event) {log(g, null, event);}

  public static void log(Game g, Object o, String event)
  {
    if (g != null)
      g.log.add(sdf.format(System.currentTimeMillis()) + " [" + (o == null ? "" : o.hashCode()) + "]: " + event);
  }

  // constants
  public static final int PLAYERS = 3;
  public static final int TALON_SIZE = 2;
  public static final int MAXIMUM_TRICKS = 10;
  public static final int MAXIMUM_ROUNDS = 10;
  public static final int HAND_SIZE = 10;

  public static final int NO_ID = -1;

  // JSON serializer
  public static final Gson gson = new Gson();

  public static final class Error extends Exception
  {
    public Error() {}
    public Error(Throwable cause) {super(cause);}
  }

  // listener for various game events and actions
  public static interface Listener
  {
    // menu actions
    void enableLastTrickMenu(boolean enabled);

    // game resume events
    void onGameRestoreStart(Game g);
    void onGameRestoreEnd(Game g);

    // replay events
    void onBeforeReplayStart(Game g);
    void onReplayStart();
    void onReplayEnd();

    // input option events
    void onBeforeShowInputOptions(Game g, Settings settings);
    void showBidPlay(Bid bid);
    void showBidMisere();
    void showBidPass();
    void showBidPass(int factor);
    void showDrop();
    void showWithout3();
    void showContractPlay(Game g, Bid bid);
    void showWhistFull();
    void showWhistHalf();
    void showWhistPass();
    void showOpenStand();
    void showTricking10Confirm();
    void showMisereConfirm();
    void showOffer();
    void enableCards(int player, int hand);

    // player actions
    void showTurn(int player);
    void showPlayerCards(Game g, Settings settings, int player);
    void disablePlayerCards(int player);
    void showTrick(int trick);
    void setTrickCount(int player, int tricks);

    // first hand actions
    void showFirstHand(int player);
    void hideFirstHand();

    // dealing events
    void onBeforeDeal();
    void onAfterDeal(Game g);

    // bidding actions
    void onBiddingStart(Game g);
    void onPlayerBid(Player p, BidData bid);
    void onFirstHumanBid(Game game, Player human);
    void showBid(Player p, Game.BidData bid, boolean animation);
    void hideBids(Game g);

    // talon events & actions
    void onTalonShow(Game g, Settings settings);
    void onTalonTaken(Game g, Settings settings);
    void onTalonCardPlayed(Settings settings, int round);
    void setTalon(int[] talon);
    void closeTalonCard(int i);
    void openTalonCard(int i);
    void hideTalonCard(int i);

    // dropping events
    void onPlayerDrop(Game g, Settings settings, Player p);

    // contract events
    void onPlayerContract(Player p, BidData bid);
    void onOpenGameType(Game g, Settings settings, Player opponent1, Player opponent2);
    void onStandingGameType() throws Error;

    // player's events
    void onPlayCard(Game g, Player player, Card card);
    void onAfterMovingRoundEnd(Game game, Settings settings, Player winner);
    void onOfferDeclined();

    // misere desk actions
    void showMisereDesk(Game g, Settings settings, int hand);
    void markMisereMove(int move);

    // 'over' events
    void onScore(Game g);
    void onSetOver(Game g);
    void onShowInitialPosition(Game g, Settings settings);
    void onGameOver(Game g);

    // 'tricking 10' events
    void onTrickingTenCheckStarted(Game g);
    void onTrickingTenCheckRequested(Game g);
    void onTrickingTenCheckFinished(Game g);

    // fast moving events
    void onFastMovingStarted(Game g);
    void onFastMovingRequested(Game g);
    void onFastMovingFinished(Game g);

    // resolver events & actions
    void onResolverInitialize(Game g, Settings settings) throws Error;
    void onRecalculateOnNextSolveRequested(int trick) throws Error;
    void onBestScoreRequested(Game g) throws Error;
    void onSolveRequested(Game g, Settings settings, Player mover) throws Error;
    void onResolverAdvance(Game g, Settings settings, int move) throws Error;
    void onResolverRestoreRequested(Game g, Settings settings);

    // tutorial 'show' events
    void showIntroduction(Game g, Settings settings);
    void showNote(Scenario.Note note);

    // tutorial mark input option events
    void markBidPlay();
    void markBidMisere();
    void markBidPass();
    void markDrop();
    void markWhistFull();
    void markWhistHalf();
    void markWhistPass();
    void markMiserePlay();
    void markStand(boolean stand);
    void markCard(int player, Card c, int i);
    void markCard(int player, Card c);

    // tutorial invalid input events
    void onInvalidBid();
    void onInvalidDrop();
    void onInvalidContract();
    void onInvalidGameType();
    void onInvalidMove(Game g, Settings settings, int player);
  }

  // game type in "moving" mode
  public enum Type {UNKNOWN, STANDING, OPEN}

  public static class HandData
  {
    public int initial;
    public int current;
    public boolean open;
    public final ArrayList<Card.Suit> suits = new ArrayList<Card.Suit>(Card.Suit.rising.length);

    private void reset(boolean open)
    {
      // do not clean initial, it is required to be left intact in the "Begin" state
      //initial = 0;
      current = 0;
      this.open = open;
      suits.clear();
    }
  }

  // bid container
  public static class BidData
  {
    public Bid current;
    public Bid max;
    // bid count
    public int count;
    public boolean visible;

    public void reset()
    {
      current = max = null;
      count = 0;
      visible = false;
    }

    @Override
    public String toString()
    {
      return current + "/" + max;
    }
  }

  public static class TrickData
  {
    // hash value of the previous trick
    public int last;
    // current trick
    public int current;

    public void reset()
    {
      last = 0;
      current = 0;
      list.clear();
    }

    public int trash(int trash)
    {
      // copy current trick data to the previous one
      last = current;
      // add trick to the list
      list.add(last);
      // trash current trick cards
      trash = Trick.trash(current, trash);
      // reset current trick
      current = 0;

      return trash;
    }

    public int tricks(int id, Card.Suit trump)
    {
      int tricks = 0;

      for (int hash : list)
        if (Trick.winner(hash, trump) == id)
          ++tricks;

      return tricks;
    }

    public final ArrayList<Integer> list() {return list;}

    // list of tricks in the game
    private final ArrayList<Integer> list = new ArrayList<Integer>(Game.HAND_SIZE);
  }

  public static final class ServiceData
  {
    public long memory;
    public int nodes;
    public int moves;
  }

  // must be static, otherwise GSON deserializes it incorrect (this equals to null!)
  public static final class PlayerData
  {
    // game dealer
    public int dealer = Game.NO_ID;
    // starting player and hand (player can play other player's hand)
    public int starting = Game.NO_ID;
    // highest bidder - determined in contract mode (or null if going to play passing)
    public int declarer = Game.NO_ID;
    // player who plays against the declarer (the main whister)
    public int opponent = Game.NO_ID;
    // player who plays against the declarer (the passer)
    public int passer = Game.NO_ID;
    // player declared half whist
    public int half = Game.NO_ID;
    // current player and hand (player can play other player's hand)
    public int current = Game.NO_ID;

    // cards in player possession (hash for each suit)
    public HandData[] hands = new HandData[PLAYERS];
    // last and max bids - becomes contract after contracting mode
    public BidData[] bids = new BidData[PLAYERS];
    // player scores
    private Score[] scores = new Score[PLAYERS];
    // tricks taken so far in the current set
    public int[] tricks = new int[PLAYERS];

    public PlayerData()
    {
      // set the first dealer to be random
      dealer = Deck.instance().getRandomDealer();

      for (int i = 0; i < PLAYERS; ++i)
      {
        hands[i] = new HandData();
        bids[i] = new BidData();
        scores[i] = new Score();
      }
    }

    public void reset(int id, boolean open)
    {
      // clear cards
      hands[id].reset(open);
      // clear bids
      bids[id].reset();
      // clear tricks count
      tricks[id] = 0;
    }
  }

  // create player data object
  public final PlayerData player = new PlayerData();

  // tricks (previous and current) used in card modes (misere, passing, tricking)
  public TrickData trick = new TrickData();
  // latest bid
  public Bid bid;
  // current trick - used in card modes (misere, passing, tricking)
  //public Trick trick = new Trick();
  // current hand index - this is how players know what hand are they when it's time to make a move
  public int hand;
  // rounds played in the set - used in card modes
  public int rounds;
  // number of passers in the current round - used in bidding mode
  public int passers;
  // number of whisters in the current round - used in contracting mode
  public int whisters;
  // number of sets played in the game
  public int sets;
  // consequent passing sets
  public int consequentPassingSets;
  // cards that are out of the game
  public int trash;
  // current game type
  public Type type;
  // current state
  public String state;
  // current mode
  public String mode;
  // input options
  public Input.Options options = new Input.Options();
  // is it replay
  public boolean replay;
  // game's statistics
  public Statistics statistics = new Statistics();
  // should 'tricking 10' game be checked by human
  public boolean check10;
  // input
  public Input input = new Input();
  // system log
  public List<String> log = new LinkedList<String>();
  // scenario ID in case it is a tutorial game
  public int scenario = Scenario.NO_ID;

  // service data
  public transient ServiceData service = new ServiceData();

  public Game(Settings settings, int scenario)
  {
    this.scenario = scenario;

    // create players
    createPlayers();

    // initialize player mountains using Rostov's rules
    if (settings.isRostov())
      for (Player player : players())
        getScore(player.id()).mountain.add(settings.poolSize / 2);

    // new game always starts from dealing mode
    set(Mode.dealing);

    // set dealer from scenario in case of tutorial
    if (tutorial())
    {
      player.dealer = scenario().dealer();
      set(State.introduction);
    }
  }

  public void reset()
  {
    // clear input options
    options.clear();
    // clear input
    input.reset();

    // clear drop
    for (int i = 0; i < Game.TALON_SIZE; ++i)
      setDrop(i, 0);

    // reset trick
    trick.reset();

    // reset players
    for (Player p : players)
      p.reset(this);

    // clear declarer from previous set
    player.declarer = player.opponent = player.passer = player.half = player.current = Game.NO_ID;
    // clear game standing/open mode
    type = Game.Type.UNKNOWN;
    // reset number of passers
    passers = 0;
    // clear highest bid
    bid = null;
    // clear check 10 flag
    check10 = false;
    // syslog
    log.clear();
  }

  public Game clone() {return gson.fromJson(gson.toJson(this), this.getClass());}

  public boolean tutorial() {return scenario != Scenario.NO_ID;}

  public boolean isOpen() {return type == Type.OPEN;}

  // get talon
  public int[] talon() {return talon;}
  // set talon's card
  public void setTalon(int i, int hash) {talon[i] = hash;}
  // get talon's card hash
  public int getTalonHash(int i) {return talon[i];}
  // get talon's card compressed
  public byte getTalonCompressed(int i) {return Card.card(talon[i]).compressed;}

  // get drop
  public int[] drop() {return drop;}
  // get drop's card hash
  public int getDropHash(int i) {return drop[i];}
  // set drop's card
  public void setDrop(int i, int hash) {drop[i] = hash;}
  public void setDrop(int[] drop) {System.arraycopy(drop, 0, this.drop, 0, drop.length);}

  // get drop for resolver
  public int getResolverDrop()
  {
    Player declarer = declarer();

    if (check10 || (declarer != null && !Player.isHuman(declarer)))
      return getDropHash(0) | getDropHash(1);

    return 0;
  }

  // get human player (the one with id 0)
  public final Player human() {return player(0);}

  // score game
  public void score() {set(State.score);}

  // end the game
  public void end() {set(State.showPool1);}

  // change current state
  public void set(State state)
  {
    // save the real game state in the "resume" state
    if (state == State.resume)
      state.next(state());

    // set game state name
    this.state = state.getClass().getName();
  }

  // get current state
  public State state() {return State.get(state);}

  // change current mode
  public void set(Mode mode)
  {
    this.mode = mode.getClass().getName();
    // new mode requires changing state to Initializing
    set(State.initialize);
  }

  // get current mode
  public Mode mode() {return Mode.get(mode);}

  // get current trump
  public Card.Suit trump() {return player.declarer == Game.NO_ID ? Card.Suit.NONE : getDeclarerBid().current.trump;}

  // players methods
  public final Player[] players()
  {
    createPlayers();

    return players;
  }

  public Player player(int id)
  {
    createPlayers();

    return NO_ID < id && id < Game.PLAYERS ? players[id] : null;
  }

  // get next player (on the left)
  public Player next(Player player) {return this.player(next(player.id()));}
  public static int next(int player) {return (player + 1) % PLAYERS;}
  // get previous player (on the right)
  public Player previous(Player player) {return this.player(previous(player.id()));}
  public static int previous(int player) {return --player < 0 ? PLAYERS - 1 : player;}

  // make a move
  public void move(int move, Player player)
  {
    Card c = Card.card(move);

    Log.debug(Log.TAG, player + " plays " + c);

    trick.current = Trick.add(trick.current, player.id(), c.compressed);

    // remove card from player's hand
    HandData hand = this.player.hands[player.id()];
    hand.current = Hand.remove(hand.current, move);
  }

  public Player dealer() {return player(player.dealer);}
  public Player declarer() {return player(player.declarer);}
  public Player opponent() {return player(player.opponent);}
  public Player passer() {return player(player.passer);}
  public Player half() {return player(player.half);}
  public Player starting() {return player(player.starting);}
  public Player current() {return player(player.current);}

  public int getFirstHandPlayer() {return Game.next(player.dealer);}

  // get declarer player hand
  public HandData getDeclarerHand() {return player.hands[player.declarer];}
  // get human player hand
  public HandData getHumanHand() {return player.hands[0];}
  // get current player hand
  public HandData getCurrentPlayerHand() {return player.hands[player.current];}
  // get opponent player hand
  public HandData getOpponentHand() {return player.hands[player.opponent];}
  // get passer player hand
  public HandData getPasserHand() {return player.hands[player.passer];}
  // get player hand
  public HandData getHand(int id) {return player.hands[id];}

  // get declarer player bid
  public BidData getDeclarerBid() {return getBid(player.declarer);}
  // get human player bid
  public BidData getHumanBid() {return getBid(0);}
  // get current player bid
  public BidData getCurrentPlayerBid() {return getBid(player.current);}
  // get half-whist player bid
  public BidData getHalfBid() {return getBid(player.half);}
  // get passer player bid
  public BidData getPasserBid() {return getBid(player.passer);}
  // get current player bid
  public BidData getBid(int id) {return player.bids[id];}
  // set player bids
  public void setBid(int id, Bid current, Bid max) {setCurrentBid(id, current); setMaxBid(id, max);}
  // set player current bid
  public void setCurrentBid(int id, Bid bid) {player.bids[id].current = bid;}
  // set player max bid
  public void setMaxBid(int id, Bid bid) {player.bids[id].max = bid;}

  // get declarer player number of tricks
  public int getDeclarerTricks() {return getTricks(player.declarer);}
  // get opponent player number tricks
  public int getOpponentTricks() {return getTricks(player.opponent);}
  // get passer player number tricks
  public int getPasserTricks() {return getTricks(player.passer);}
  // get player number of tricks
  public int getTricks(int id) {return player.tricks[id];}
  // set tricks of player
  public void setTricks(int id, int tricks) {player.tricks[id] = tricks;}
  // update tricks of player by provided delta
  public void addTricks(int id, int tricks) {player.tricks[id] += tricks;}

  // get human player score
  public Score getHumanScore() {return getScore(0);}
  // get player score
  public Score getScore(int id) {return player.scores[id];}
  // get contract
  public int contract() {return getDeclarerBid().current.tricks;}

  // test if half whist might be declared
  public boolean canHalfWhist() {return contract() == 6 || getDeclarerBid().current.tricks == 7;}

  public int getStartingTricks(Settings settings)
  {
    switch (settings.passingExit)
    {
      case PE_6666:
        return 6;

      case PE_6777:
        return consequentPassingSets == 0 ? 6 : 7;

      case PE_6788:
        return consequentPassingSets == 0 ? 6 : (consequentPassingSets == 1 ? 7 : 8);

      case PE_678678:
        int mod = consequentPassingSets % 3;
        return mod == 0 ? 6 : (mod == 1 ? 7 : 8);
    }

    // should never reach
    return 0;
  }

  public Scenario scenario() {return Scenario.get(scenario);}

  private void createPlayers()
  {
    if (players == null)
    {
      players = new Player[Game.PLAYERS];

      players[0] = new Human(this, 0, "Me");
      players[1] = tutorial() ? new Actor(this, 1, "O1") : new AI(this, 1, "O1", Strategy.SIMPLE);
      players[2] = tutorial() ? new Actor(this, 2, "O2") : new AI(this, 2, "O2", Strategy.SIMPLE);
    }
  }

  // for logging
  private static final SimpleDateFormat sdf = new SimpleDateFormat("dd/MM-HH:mm:ss:SSS");

  private Game() {}

  // players
  private transient Player[] players;
  // talon cards hashes
  private int[] talon = new int[Game.TALON_SIZE];
  // drop cards hashes
  private int[] drop = new int[Game.TALON_SIZE];

/*
  public static void main(String[] argv)
  {
    Game g = new Game();

    String s = Game.gson.toJson(g);
    System.out.println(s);

    g = Game.gson.fromJson(s, Game.class);
    System.out.println(g.players + ", " + g.service);
  }
*/
}
